Skip to content

feat: store sent notifications to DB - #52

Open
busehalis-sap wants to merge 13 commits into
mainfrom
feat/store-notifications-db
Open

feat: store sent notifications to DB#52
busehalis-sap wants to merge 13 commits into
mainfrom
feat/store-notifications-db

Conversation

@busehalis-sap

@busehalis-sap busehalis-sap commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Some applications need the notifications stored to DB to do further processing with them. This PR adds optional DB storage for sent notifications in both production and local mode.

When cds.notifications.storeNotifications: true is set, each notification is stored to the database after being sent. In production mode, the ANS-assigned notification ID is used directly. In local mode, a UUID is generated locally. Since a single notification can be sent to multiple recipients, one row per recipient is created.

Changes

  • NotificationStorage.cds: CDS model with @PersonalData annotations for Notifications, NotificationProperties and NotificationTargetParameters entities
  • StoreNotificationsHandler: @After handler on NotificationProviderService that stores notifications to DB after ANS delivery in production mode
  • StoreNotificationsLocalHandler: @After handler on ApplicationService that stores notifications to DB in local mode
  • NotificationStorageHelper: shared helper class containing the DB persistence logic used by both handlers
  • LocalHandler: sets the sent notifications on the event context so StoreNotificationsLocalHandler can access them
  • NotificationServiceConfiguration: reads storeNotifications flag and conditionally registers the appropriate handler based on the current mode
  • Integration tests: 4 new tests covering DB storage, properties, target parameters and multiple recipients, and 1 test for local mode
  • Sample app: demonstrates the feature with StoredNotifications projection and key bookId field

@busehalis-sap
busehalis-sap requested a review from lisajulia July 27, 2026 13:27

@lisajulia lisajulia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small changes, otherwise this is fine :)
Thanks!


Instant sentAt = Instant.now();

for (int i = 0; i < results.size(); i++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly add a check here that results and entries have the same size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the size check suggestion! Looking at this more carefully, I realized the loop itself is unnecessary.

In ProductionHandler, each notification is inserted individually in a loop:

for (int i = 0; i < results.size(); i++) {
    notificationProviderService.run(Insert.into(Notifications_.CDS_NAME).entry(notification));
}

Each run(Insert...) call is a separate OData POST request. Since StoreNotificationsHandler is an @after(EVENT_CREATE) handler on NotificationProviderService, it is triggered after each individual run() call, not once for the entire batch.

I simplified the handler to use results.get(0) and entries.get(0) directly, and removed the loop. The size check is also not needed for the same reason. results.size() will always equal entries.size() (both always 1), since this handler fires per individual insert, not per batch.

this.storageService = storageService;
}

@After(event = "*")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be after every event, right? Is it possible to narrow that down?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narrowing it down would require more complex architectural changes. The filtering already happens at the top of the method with a simple map lookup, which is very cheap. EntityNotificationHandler uses the same pattern.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it affects local only therefore not that relevant anyway. Tested it out and it worked :)

priority : String(20);
navigationTargetObject : String(500);
navigationTargetAction : String(500);
sentAt : Timestamp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention in the readme that this is UTC time.

@Schmarvinius Schmarvinius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just minor stuff

Comment on lines +29 to +30
@After(event = CqnService.EVENT_CREATE, entity = Notifications_.CDS_NAME)
public void storeNotifications(CdsCreateEventContext context) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be simplified

Suggested change
@After(event = CqnService.EVENT_CREATE, entity = Notifications_.CDS_NAME)
public void storeNotifications(CdsCreateEventContext context) {
@After
public void storeNotifications(CdsCreateEventContext context, List<CdsData> data) {

configurer
.getCdsRuntime()
.getEnvironment()
.getProperty("cds.requires.notifications.storeNotifications", Boolean.class, false);

@Schmarvinius Schmarvinius Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cds.requires is the namespace for require service confiugration -> feature toggles are in cds.

Suggested change
.getProperty("cds.requires.notifications.storeNotifications", Boolean.class, false);
.getProperty("cds.notifications.storeNotifications", Boolean.class, false);

import org.slf4j.LoggerFactory;

/** Shared service for persisting notifications to the database. */
public class NotificationStorageService {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class isn't a service (in cap terms) it is a normal pojo

@Schmarvinius Schmarvinius left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think its fine but please await lisas review as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants